/*

<This Java Class is part of the jMusic API version 1.5, March 2004.>


Copyright (C) 2000 Andrew Sorensen, Andrew Brown, Adam Kirby

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or any
later version.

This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

*/

package jm.music.data;

import jm.JMC;

import java.io.Serializable;
import java.util.Enumeration;
import java.util.Vector;

/**
 * The Score class is used to hold score data.  Score data includes
 * is primarily made up of a vector of Part objects. Commonly score
 * data is algorithmically generated or read from a standard MIDI file, but can also be read and saved to
 * file using Java's object serialization. In this way a Score's data can
 * be saved in a more native context.
 * To find out how to read from and write to standard MIDI files
 * or to use object serializationcheck out
 * the jm.util.Read and km.util.Write classes.
 *
 * @author Andrew Sorensen, Andrew Brown, Adam Kirby
 * @version 1.0, Sun Feb 25 18:43:33  2001
 * @see Part
 * @see jm.midi.SMF
 */
public class Score implements JMC, Cloneable, Serializable {

    //----------------------------------------------
    // Defaults
    //----------------------------------------------

    public static final String DEFAULT_TITLE = "Untitled Score";

    public static final double DEFAULT_TEMPO = 60.0;

    public static final int DEFAULT_KEY_SIGNATURE = 0;

    public static final int DEFAULT_KEY_QUALITY = 0;

    public static final int DEFAULT_NUMERATOR = 4;

    public static final int DEFAULT_DENOMINATOR = 4;

    //----------------------------------------------
    // Attributes
    //----------------------------------------------

    /**
     * the name assigned to a Score
     */
    private String title = "Unnamed Score";
    /**
     * a Vector containing the Part objects associated with this score
     */
    private Vector partList;
    /**
     * the speed for this score
     */
    private double tempo;

    // Possible Alternative:
    //      Consider using the jm.music.data.KeySignature class and modifying
    //      it to support the keyQuality field.

    /**
     * the number of accidentals this score
     * negative numbers are Flats, positive numbers are Sharps
     */
    private int keySignature;
    /**
     * 0 = major, 1 = minor, others modes not specified
     */
    private int keyQuality;

    // Possible Alternative:
    //      Consider making a TimeSignature class, containing the following
    //      two fields.

    /**
     * the top number of the time signature
     */
    private int numerator;
    /**
     * the bottom number of the time signature
     */
    private int denominator;

    //----------------------------------------------
    // Constructors
    //----------------------------------------------

    /**
     * Constructs an empty score with a default name
     */
    public Score() {
        this(DEFAULT_TITLE);
    }

    /**
     * Constructs an empty score.
     *
     * @param title Give the score a name
     */
    public Score(String title) {
        this(title, DEFAULT_TEMPO);
    }

    /**
     * Constructs an empty score at the specified tempo
     *
     * @param tempo The speed for this score in beats per minute
     */
    public Score(double tempo) {
        this(DEFAULT_TITLE, tempo);
    }

    /**
     * Constructs an empty score.
     *
     * @param title Give the score a name
     * @param tempo Define speed of playback in bpm
     */
    public Score(String title, double tempo) {
        this.title = title;
        this.tempo = tempo;
        this.partList = new Vector();
        this.keySignature = DEFAULT_KEY_SIGNATURE;
        this.keyQuality = DEFAULT_KEY_QUALITY;
        this.numerator = DEFAULT_NUMERATOR;
        this.denominator = DEFAULT_DENOMINATOR;
    }

    /**
     * Constructs a Score containing the specified <CODE>part</CODE>.
     *
     * @param Part to be contained in the Score
     */
    public Score(Part part) {
        this();
        if (part.getTempo() > 0) {
            this.tempo = part.getTempo();
        } else this.tempo = tempo;
        this.addPart(part);
    }

    /**
     * Constructs a Score containing the specified <CODE>part</CODE>.
     *
     * @param title Give the score a name
     * @param tempo Define speed of playback in bpm
     * @param part  The Part to be contained in the Score
     */
    public Score(String title, double tempo, Part part) {
        this(title, tempo);
        this.addPart(part);
    }


    /**
     * Constructs a Score containing the specified <CODE>parts</CODE>.
     *
     * @param parts - array of Parts to be contained in the Score
     */
    public Score(Part[] parts) {
        this();
        addPartList(parts);
    }

    /**
     * Constructs a Score containing the specified <CODE>part</CODE> with
     * the specified <CODE>title</CODE>.
     *
     * @param part  Part to be contained in the Score
     * @param title String describing the title of the Score
     */
    public Score(Part part, String title) {
        this(title, part.getTempo());
        addPart(part);
    }

    /**
     * Constructs a Score containing the specified <CODE>parts</CODE> with
     * the specified <CODE>title</CODE>.
     *
     * @param parts Array of Parts to be contained in the Score
     * @param title String describing the title of the Score
     */
    public Score(Part[] parts, String title) {
        this(title);
        addPartList(parts);
    }

    /**
     * Constructs a Score containing the specified <CODE>part</CODE> with
     * the specified <CODE>title</CODE> and the specified <CODE>tempo</CODE>.
     *
     * @param part  Part to be contained in the Score
     * @param title String describing the title of the Score
     * @param tempo A double describing the tempo of the Score
     */
    public Score(Part part, String title, double tempo) {
        this(title, tempo);
        addPart(part);
    }

    /**
     * Constructs a Score containing the specified <CODE>parts</CODE> with
     * the specified <CODE>title</CODE> and the specified <CODE>tempo</CODE>.
     *
     * @param parts array of Parts to be contained in the Score
     * @param title String describing the title of the Score
     * @param tempo double describing the tempo of the Score
     */
    public Score(Part[] parts, String title, double tempo) {
        this(title, tempo);
        addPartList(parts);
    }


    //----------------------------------------------
    // Data Methods
    //----------------------------------------------

    /**
     * Add a Track object to this Score
     */
    public void add(Part part) {
        this.addPart(part);
    }

    /**
     * Add a Track object to this Score
     */
    public void addPart(Part part) {
        part.setMyScore(this);
        this.partList.addElement(part);
    }

    /**
     * Inserts <CODE>part</CODE> at the specified position, shifting all parts
     * with indices greater than or equal to <CODE>index</CODE> up one position.
     *
     * @param part  Part to be added
     * @param index where it is to be inserted
     * @throws ArrayIndexOutOfBoundsException when <CODE>index</CODE> is beyond the range of current
     *                                        parts.
     */
    public void insertPart(Part part, int index)
            throws ArrayIndexOutOfBoundsException {
        this.partList.insertElementAt(part, index);
        part.setMyScore(this);
    }

    /**
     * Adds multiple parts to the score from an array of parts
     *
     * @param partArray
     */
    public void addPartList(Part[] partArray) {
        for (int i = 0; i < partArray.length; i++) {
            this.addPart(partArray[i]);
        }
    }

    /**
     * Deletes the specified Part in the Score
     *
     * @param int partNumb the index of the part to be deleted
     */
    public void removePart(int partNumb) {
        Vector vct = (Vector) this.partList;
        try {
            vct.removeElement(vct.elementAt(partNumb));
        } catch (RuntimeException re) {
            System.err.println("The Part index to be deleted must be within the score.");
        }
    }

    /**
     * Deletes the first occurence of the specified part in the Score.
     *
     * @param part the Part object to be deleted.
     */
    public void removePart(Part part) {
        this.partList.removeElement(part);
    }

    /**
     * Deletes the last Part added to the Score
     */
    public void removeLastPart() {
        Vector vct = (Vector) this.partList;
        vct.removeElement(vct.lastElement());
    }

    /**
     * Deletes all the parts previously added to the score
     */
    public void removeAllParts() {
        this.partList.removeAllElements();
    }

    /**
     * Returns the Scores List of Tracks
     */
    public Vector getPartList() {
        return partList;
    }

    /**
     * Returns the all Parts in this Score as a array
     *
     * @return Part[] An array containing all Part objects in this score
     */
    public Part[] getPartArray() {
        Vector vct = (Vector) this.partList.clone();
        Part[] partArray = new Part[vct.size()];
        for (int i = 0; i < partArray.length; i++) {
            partArray[i] = (Part) vct.elementAt(i);
        }
        return partArray;
    }

    /**
     * Get an individual Track object from its title
     *
     * @param String title - the name of the Track to return
     * @return Track answer - the Track to return
     */
    public Part getPart(String title) {
        Enumeration enum1 = partList.elements();
        while (enum1.hasMoreElements()) {
            Part part = (Part) enum1.nextElement();
            if (part.getTitle().equalsIgnoreCase(title)) {
                return part;
            }
        }
        return null;
    }

    /**
     * Get an individual Track object from its number
     *
     * @param int number - the number of the Track to return
     * @return Track answer - the Track to return
     */
    public Part getPart(int number) {
        Enumeration enum1 = partList.elements();
        int counter = 0;
        while (enum1.hasMoreElements()) {
            Part part = (Part) enum1.nextElement();
            if (counter == number) {
                return part;
            }
            counter++;
        }
        return null;
    }


    //----------------------------------------------
    // Utility Methods
    //----------------------------------------------

    /**
     * Return the title of this Score
     *
     * @return String title - the name of this Score
     */
    public String getTitle() {
        return title;
    }

    /**
     * Assign a title to this Score
     *
     * @param String title - the name of this Score
     */
    public void setTitle(String title) {
        this.title = title;
    }

    /**
     * Returns the Score's tempo
     *
     * @return double tempo
     */
    public double getTempo() {
        return this.tempo;
    }

    /**
     * Sets the Score's tempo
     *
     * @param double tempo
     */
    public void setTempo(double tempo) {
        this.tempo = tempo;
    }

    /**
     * '
     * Returns the Score's key signature
     * The number of sharps (+) or flats (-)
     *
     * @return int key signature
     */
    public int getKeySignature() {
        return this.keySignature;
    }

    /**
     * Specifies the Score's key signature
     * The number of sharps (+) or flats (-)
     *
     * @param int key signature
     */
    public void setKeySignature(int newSig) {
        this.keySignature = newSig;
    }

    /**
     * Returns the Score's key quality
     * 0 is Major, 1 is minor
     *
     * @return int key quality
     */
    public int getKeyQuality() {
        return this.keyQuality;
    }

    /**
     * Specifies the Score's key quality
     * 0 is Major, 1 is minor
     *
     * @param int key quality (modality)
     */
    public void setKeyQuality(int newQual) {
        this.keyQuality = newQual;
    }

    /**
     * Specifies the Score's time signature
     *
     * @param num - Time signature numerator
     * @param dem - Time signature denominator
     */
    public void setTimeSignature(int num, int dem) {
        this.numerator = num;
        this.denominator = dem;
    }

    /**
     * Returns the Score's time signature elements.
     *
     * @return Point The Time Signature elements packaged together with the
     * numerator as'x' and denominator as 'y'. You may prefer to use the
     * getNumerator and getDenomintor methods directly, rather than use
     * this convenience method which requires importing the java.awt package
     * to extract the elements : )
     */
    public java.awt.Point getTimeSignature() {
        return new java.awt.Point(this.numerator, this.denominator);
    }

    /**
     * Retrieves the numerator element of the Score's time signature.
     *
     * @return num - Time signature numerator
     */
    public int getNumerator() {
        return this.numerator;
    }

    /**
     * Specifies the Score's time signature numerator
     *
     * @param int time signature numerator
     */
    public void setNumerator(int num) {
        this.numerator = num;
    }

    /**
     * Retrieves the denominator element of the Score's time signature.
     *
     * @return int Time signature denominator
     */
    public int getDenominator() {
        return this.denominator;
    }

    /**
     * Specifies the Score's time signature denominator
     *
     * @param int time signature denominator
     */
    public void setDenominator(int dem) {
        this.denominator = dem;
    }

    /**
     * Make a copy of this Score object
     *
     * @return Score - return a new Score Object
     */
    public Score copy() {
        Score newScore = new Score(title + " copy");
        newScore.setTempo(this.tempo);
        newScore.setTimeSignature(this.numerator, this.denominator);
        Enumeration enum1 = this.partList.elements();
        while (enum1.hasMoreElements()) {
            Part oldPart = (Part) enum1.nextElement();
            newScore.addPart((Part) oldPart.copy());
        }
        return (Score) newScore;
    }

    public Score copy(final double startTime, final double endTime) {
        Score score = this.copy();
        score.removeAllParts();
        score.setTempo(this.tempo);
        score.setTimeSignature(this.numerator, this.denominator);
        int scoresize = this.size();
        for (int i = 0; i < scoresize; i++) {
            score.addPart(this.getPart(i).copy(startTime, endTime));
        }
        return score;
    }

    /**
     * Return the beat where score ends. Where it's last Part ends.
     *
     * @return double the Parts endTime
     */
    public double getEndTime() {
        double endTime = 0.0;
        Enumeration enum1 = this.partList.elements();
        while (enum1.hasMoreElements()) {
            Part nextPart = (Part) enum1.nextElement();
            double partEnd = nextPart.getEndTime();
            if (partEnd > endTime) endTime = partEnd;
        }
        return endTime;
    }


    /**
     * Print the titles of all tracks to stdout
     */
    public String toString() {
        String scoreData = new String("***** jMusic SCORE: '" + title +
                "' contains " + this.size() + " parts. ****" + '\n');
        scoreData += "Score Tempo = " + this.tempo + " bpm" + '\n';
        Enumeration enum1 = partList.elements();
        while (enum1.hasMoreElements()) {
            Part part = (Part) enum1.nextElement();
            scoreData = scoreData + part.toString() + '\n';
        }
        return scoreData;
    }

    /**
     * Empty removes all elements in the vector
     */
    public void empty() {
        this.empty(false);
    }

    /**
     * Empty removes all elements in the vector.
     *
     * @param nullObjects If ture this sets all jMusic data objects to null
     *                    priot to removing. This facilitates garbage collection.
     */
    public void empty(boolean nullObjects) {
        if (nullObjects) {
            Enumeration enum1 = getPartList().elements();
            while (enum1.hasMoreElements()) {
                Part part = (Part) enum1.nextElement();
                Enumeration enum2 = part.getPhraseList().elements();
                while (enum2.hasMoreElements()) {
                    Phrase phrase = (Phrase) enum2.nextElement();
                    Enumeration enum3 = part.getPhraseList().elements();
                    while (enum3.hasMoreElements()) {
                        Note note = (Note) enum3.nextElement();
                        note = null;
                    }
                    phrase = null;
                }
                part = null;
            }
        }
        partList.removeAllElements();
    }

    /**
     * Get the number of Parts in this score
     *
     * @return int  The number of parts
     */
    public int length() {
        return size();
    }

    /**
     * Get the number of Parts in this score
     *
     * @return int  length - the number of parts
     */
    public int size() {
        return (partList.size());
    }

    /**
     * Get the number of Parts in this score
     *
     * @return int  length - the number of parts
     */
    public int getSize() {
        return (partList.size());
    }

    /**
     * Remove any empty Parts or phrases from the Score.
     */
    public void clean() {
        Enumeration enum1 = getPartList().elements();
        while (enum1.hasMoreElements()) {
            Part part = (Part) enum1.nextElement();
            // pass on the part to have phases cleaned
            part.clean();
            // check if part is empty
            if (part.getPhraseList().size() == 0) {
                this.removePart(part);
            }
        }
    }

    /**
     * Return the value of the highest note in the Score.
     */
    public int getHighestPitch() {
        int max = 0;
        Enumeration enum1 = getPartList().elements();
        while (enum1.hasMoreElements()) {
            Part part = (Part) enum1.nextElement();
            if (part.getHighestPitch() > max) max = part.getHighestPitch();
        }
        return max;
    }

    /**
     * Return the value of the lowest note in the Score.
     */
    public int getLowestPitch() {
        int min = 127;
        Enumeration enum1 = getPartList().elements();
        while (enum1.hasMoreElements()) {
            Part part = (Part) enum1.nextElement();
            if (part.getLowestPitch() < min) min = part.getLowestPitch();
        }
        return min;
    }

    /**
     * Return the value of the longest rhythm value in the Score.
     */
    public double getLongestRhythmValue() {
        double max = 0.0;
        Enumeration enum1 = getPartList().elements();
        while (enum1.hasMoreElements()) {
            Part part = (Part) enum1.nextElement();
            if (part.getLongestRhythmValue() > max) max = part.getLongestRhythmValue();
        }
        return max;
    }

    /**
     * Return the value of the shortest rhythm value in the Score.
     */
    public double getShortestRhythmValue() {
        double min = 1000.0;
        Enumeration enum1 = getPartList().elements();
        while (enum1.hasMoreElements()) {
            Part part = (Part) enum1.nextElement();
            if (part.getShortestRhythmValue() < min) min = part.getShortestRhythmValue();
        }
        return min;
    }

    /**
     * Determine the pan position for all notes in this Score.
     *
     * @param double the phrase's pan setting
     */
    public void setPan(double pan) {
        Enumeration enum1 = partList.elements();
        while (enum1.hasMoreElements()) {
            Part part = (Part) enum1.nextElement();
            part.setPan(pan);
        }
    }

    /**
     * Generates and returns a new empty part
     * and adds it to the score.
     */
    public Part createPart() {
        Part p = new Part();
        this.addPart(p);
        return p;
    }
}
